/*
 * FluidSynthesizer.java
 *
 * This file is part of Tritonus: http://www.tritonus.org/
 */

/*
 * Copyright (c) 2006 by Henri Manson
 * Copyright (c) 2006 by Matthias Pfisterer
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/*
|<---            this code is formatted to fit into 80 columns             --->|
*/

package org.tritonus.midi.device.fluidsynth;

import javax.sound.midi.*;

import org.tritonus.share.TDebug;
import org.tritonus.share.midi.TMidiChannel;
import org.tritonus.share.midi.TDirectSynthesizer;
import org.tritonus.midi.sb.fluidsynth.FluidSoundbank;

public class FluidSynthesizer extends TDirectSynthesizer implements Synthesizer {
	private MidiChannel channels[];
	private FluidSoundbank defaultSoundbank;

	private int defaultbankSfontID;

	// native pointers 64 bit maximum
	private long settingsPtr;
	private long synthPtr;
	private long audioDriverPtr;

	static {
		loadNativeLibrary();
	}

	/**
	 * Load the native library for fluidsynth.
	 */
	private static void loadNativeLibrary() {
		if (TDebug.TraceFluidNative)
			TDebug.out("FluidSynthesizer.loadNativeLibrary(): loading native library tritonusfluid");
		try {
			System.loadLibrary("tritonusfluid");
			// only reached if no exception occures
			setTrace(TDebug.TraceFluidNative);
		} catch (Error e) {
			if (TDebug.TraceFluidNative || TDebug.TraceAllExceptions) {
				TDebug.out(e);
			}
			// throw e;
		}
		if (TDebug.TraceFluidNative)
			TDebug.out("FluidSynthesizer.loadNativeLibrary(): loaded");
	}

	/**
	 * Constructor.
	 */
	public FluidSynthesizer(MidiDevice.Info info) throws Exception {
		super(info);
	}

	protected void openImpl() throws MidiUnavailableException {
		if (newSynth() < 0) {
			throw new MidiUnavailableException("Low-level initialization of the synthesizer failed");
		}
		if (TDebug.TraceSynthesizer)
			TDebug.out("FluidSynthesizer: " + Long.toHexString(synthPtr));

		channels = new MidiChannel[16];
		for (int i = 0; i < 16; i++) {
			channels[i] = new NewFluidMidiChannel(i);
		}

		String sfontFile = System.getProperty("tritonus.fluidsynth.defaultsoundbank");
		if (sfontFile != null && !sfontFile.equals("")) {
			int sfontID = loadSoundFont(sfontFile);
			setDefaultSoundBank(sfontID);
			String strBankOffset = System.getProperty("tritonus.fluidsynth.defaultsoundbankoffset");
			if (strBankOffset != null && !strBankOffset.equals("")) {
				setBankOffset(sfontID, Integer.parseInt(strBankOffset));
			}
		}
	}

	protected void closeImpl() {
		if (TDebug.TraceSynthesizer)
			TDebug.out("FluidSynthesizer.closeImpl(): " + Long.toHexString(synthPtr));
		deleteSynth();
		super.closeImpl();
	}

	public void setDefaultSoundBank(int sfontID) {
		defaultSoundbank = new FluidSoundbank(this, sfontID);
		defaultbankSfontID = sfontID;
	}

	protected void finalize() {
		if (TDebug.TraceSynthesizer)
			TDebug.out("finalize: " + Long.toHexString(synthPtr));
		close();
	}

	public native int loadSoundFont(String filename);

	public native void setBankOffset(int sfontID, int offset);

	public native void setGain(float gain);

	/*
	 * $$mp: currently not functional because fluid_synth_set_reverb_preset() is not present in fluidsynth 1.0.6.
	 */
	public native void setReverbPreset(int reverbPreset);

	public native int getMaxPolyphony();

	protected native int newSynth();

	protected native void deleteSynth();

	/**
	 * Turns a note on.
	 * 
	 * The implementation calls fluid_synth_noteoff().
	 * 
	 * @param nChannel
	 *            the channel
	 * @param nNoteNumber
	 *            the note
	 * @param nVelocity
	 *            the velocity
	 */
	native void noteOn(int nChannel, int nNoteNumber, int nVelocity);

	/**
	 * Turns a note off.
	 * 
	 * The implementation calls fluid_synth_noteon().
	 * 
	 * @param nChannel
	 *            the channel
	 * @param nNoteNumber
	 *            the note
	 * @param nVelocity
	 *            the velocity
	 */
	native void noteOff(int nChannel, int nNoteNumber, int nVelocity);

	/**
	 * Changes a controller on the synthesizer.
	 * 
	 * The implementation calls fluid_synth_cc().
	 * 
	 * @param nChannel
	 *            the channel
	 * @param nController
	 *            the controller number
	 * @param nValue
	 *            the controller value
	 */
	native void controlChange(int nChannel, int nController, int nValue);

	/**
	 * Obtains the value of a controller.
	 * 
	 * The implementation calls fluid_synth_get_cc().
	 * 
	 * @param nChannel
	 *            the channel
	 * @param nController
	 *            the controller number
	 * @return the controller value
	 */
	native int getController(int nChannel, int nController);

	/**
	 * Sets the program for a channel.
	 * 
	 * The implementation calls fluid_synth_program_change().
	 * 
	 * @param nChannel
	 *            the channel
	 * @param nProgram
	 *            the program number
	 */
	native void programChange(int nChannel, int nProgram);

	/**
	 * Obtains the program set for a channel.
	 * 
	 * The implementation calls fluid_synth_get_program().
	 * 
	 * @param nChannel
	 *            the channel
	 * @return the program number set for this channel
	 */
	native int getProgram(int nChannel);

	/**
	 * Sets the pitch bend for a channel.
	 * 
	 * The implementation calls fluid_synth_pitch_bend().
	 * 
	 * @param nChannel
	 *            the channel
	 * @param nBend
	 *            the pitch bend value
	 */
	native void setPitchBend(int nChannel, int nBend);

	/**
	 * Obtains the pitch bend for a channel.
	 * 
	 * The implementations calls fluid_synth_get_pitch_bend().
	 * 
	 * @param nChannel
	 *            the channel
	 * @return the pitch bend value.
	 */
	native int getPitchBend(int nChannel);

	/**
	 * Sets tracing in the native code. Note that this method can either be called directly or (recommended) the system property "tritonus.TraceFluidNative" can be set to true.
	 *
	 * @see org.tritonus.share.TDebug
	 */
	public static native void setTrace(boolean bTrace);

	public boolean isSoundbankSupported(Soundbank soundbank) {
		return (soundbank instanceof FluidSoundbank);
	}

	public boolean loadAllInstruments(Soundbank soundbank) {
		checkSoundbank(soundbank);
		return true;
	}

	public void unloadAllInstruments(Soundbank soundbank) {
		checkSoundbank(soundbank);
	}

	public void unloadInstruments(Soundbank soundbank, Patch[] patchList) {
		checkSoundbank(soundbank);
	}

	public boolean loadInstruments(Soundbank soundbank, Patch[] patchList) {
		checkSoundbank(soundbank);
		return true;
	}

	public void unloadInstrument(Instrument instrument) {
		checkInstrument(instrument);
	}

	public boolean loadInstrument(Instrument instrument) {
		checkInstrument(instrument);
		return true;
	}

	public Instrument[] getAvailableInstruments() {
		return null;
	}

	public MidiChannel[] getChannels() {
		return channels;
	}

	public Soundbank getDefaultSoundbank() {
		return defaultSoundbank;
	}

	public long getLatency() {
		return 0L;
	}

	public Instrument[] getLoadedInstruments() {
		return null;
	}

	public VoiceStatus[] getVoiceStatus() {
		return new VoiceStatus[0];
	}

	public boolean remapInstrument(Instrument from, Instrument to) {
		checkInstrument(from);
		checkInstrument(to);
		return true;
	}

	/**
	 * Checks if the soundbank is supported by this synthesizer implementation.
	 * 
	 * @param sb
	 *            the soundbank to check
	 * @throws IllegalArgumentException
	 *             if the soundbank is not supported
	 */
	private void checkSoundbank(Soundbank sb) {
		if (!isSoundbankSupported(sb))
			throw new IllegalArgumentException("soundbank is not supported");
	}

	/**
	 * Checks if the instrument belongs to a soundbank that is supported by this synthesizer implementation.
	 * 
	 * @param instr
	 *            the instrument to check
	 * @throws IllegalArgumentException
	 *             if the instrument's soundbank is not supported
	 */
	private void checkInstrument(Instrument instr) {
		checkSoundbank(instr.getSoundbank());
	}

	private class NewFluidMidiChannel extends TMidiChannel {
		public NewFluidMidiChannel(int nChannel) {
			super(nChannel);
		}

		public void noteOn(int nNoteNumber, int nVelocity) {
			FluidSynthesizer.this.noteOn(getChannel(), nNoteNumber, nVelocity);
		}

		public void noteOff(int nNoteNumber, int nVelocity) {
			FluidSynthesizer.this.noteOff(getChannel(), nNoteNumber, nVelocity);
		}

		public void noteOff(int nNoteNumber) {
			noteOff(nNoteNumber, 0);
		}

		/**
		 * Fluidsynth does not implement poly pressure (aftertouch). Therefore, this method does nothing.
		 */
		public void setPolyPressure(int nNoteNumber, int nPressure) {
		}

		/**
		 * Fluidsynth does not implement poly pressure (aftertouch). Therefore, this method always return 0.
		 */
		public int getPolyPressure(int nNoteNumber) {
			return 0;
		}

		/**
		 * Fluidsynth does not implement channel pressure. Therefore, this method does nothing.
		 */
		public void setChannelPressure(int nPressure) {
		}

		/**
		 * Fluidsynth does not implement channel pressure. Therefore, this method always returns 0.
		 */
		public int getChannelPressure() {
			return 0;
		}

		public void controlChange(int nController, int nValue) {
			FluidSynthesizer.this.controlChange(getChannel(), nController, nValue);
		}

		public int getController(int nController) {
			return FluidSynthesizer.this.getController(getChannel(), nController);
		}

		public void programChange(int nProgram) {
			FluidSynthesizer.this.programChange(getChannel(), nProgram);
		}

		public int getProgram() {
			return FluidSynthesizer.this.getProgram(getChannel());
		}

		public void setPitchBend(int nBend) {
			FluidSynthesizer.this.setPitchBend(getChannel(), nBend);
		}

		public int getPitchBend() {
			return FluidSynthesizer.this.getPitchBend(getChannel());
		}

		// TODO: emulate by manipulating volume
		public void setMute(boolean bMute) {
		}

		public boolean getMute() {
			return false;
		}

		public void setSolo(boolean bSolo) {
		}

		public boolean getSolo() {
			return false;
		}
	}
}

/* FluidSynthesizer.java */
